# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Msf::Exploit::Remote::LDAP do

  subject do
    mod = ::Msf::Exploit.new
    mod.extend described_class

    mod.send(:initialize)
    mod
  end

  let(:rhost) do
    'rhost.example.com'
  end

  let(:rport) do
    1234
  end

  let(:ldap_connect_timeout) do
    30
  end

  let(:ssl) do
    false
  end

  let(:ldap_proc) do
    proc {}
  end

  before do
    subject.datastore['LDAP::ConnectTimeout'] = ldap_connect_timeout
    subject.datastore['ssl'] = ssl
    allow(subject).to receive(:rhost).and_return(rhost)
    allow(subject).to receive(:rport).and_return(rport)
  end

  after do
    # Do nothing
  end

  describe '#get_connect_opts' do
    it { expect(subject.get_connect_opts).to be_a(Hash) }
    it do
      expect(subject).to receive(:ldap_connect_opts).with(rhost, rport, ldap_connect_timeout, ssl: ssl, opts: Hash)
      subject.get_connect_opts
    end
  end

  describe '#ldap_open' do
    it do
      opts = {}
      expect(Net::LDAP).to receive(:open).with(opts, &ldap_proc)
      expect(subject).not_to receive(:get_connect_opts)
      expect(subject).to receive(:resolve_connect_opts).and_return(opts)
      subject.ldap_open(opts, &ldap_proc)
    end
  end

  describe '#ldap_connect' do
    it do
      opts = { x: 0 }
      connect_opts = { y: 1 }
      merged_opts = opts.merge(connect_opts)

      expect(subject).to receive(:ldap_open).with(merged_opts, &ldap_proc)
      expect(subject).to receive(:get_connect_opts).and_return(connect_opts)
      subject.ldap_connect(opts, &ldap_proc)
    end
  end

  describe '#ldap_new' do
    it do
      opts = { x: 0 }
      connect_opts = { y: 1 }
      merged_opts = opts.merge(connect_opts)
      expect(Net::LDAP).to receive(:new).with(merged_opts)
      expect(subject).to receive(:get_connect_opts).and_return(connect_opts)
      expect(subject).to receive(:resolve_connect_opts).and_return(merged_opts)
      subject.ldap_new(opts, &ldap_proc)
    end
  end

  describe '#resolve_connect_opts' do
    let(:cred) do
      'I am a cred'
    end

    context 'with no credential proc' do
      let(:connect_opts) do
        { auth: { initial_credential: cred } }
      end
      it { expect(subject.resolve_connect_opts(connect_opts)).to be(connect_opts) }
    end

    context 'with credential proc' do
      let(:cred_proc) do
        proc { cred }
      end

      let(:connect_opts) do
        { auth: { initial_credential: cred_proc } }
      end

      it do
        resolved_connect_opts = subject.resolve_connect_opts(connect_opts)
        expect(resolved_connect_opts[:auth][:initial_credential]).not_to be_a(Proc)
        expect(resolved_connect_opts[:auth][:initial_credential]).to eq(cred)
      end
    end
  end

  describe '#get_naming_contexts' do
    let(:ldap) do
      instance_double(Net::LDAP)
    end
    context 'Could not retrieve root DSE' do
      it do
        expect(ldap).to receive(:search_root_dse).and_return(false)
        expect(subject.get_naming_contexts(ldap)).to be(nil)
      end
    end

    context 'Empty naming contexts' do
      let(:root_dse) do
        { namingcontexts: [] }
      end
      it do
        expect(ldap).to receive(:search_root_dse).and_return(root_dse)
        expect(subject.get_naming_contexts(ldap)).to be(nil)
      end
    end

    context 'Naming contexts are present' do

      let(:naming_contexts) {
        %w[context1 context2]
      }

      let(:root_dse) do
        { namingcontexts: naming_contexts }
      end

      it do
        expect(ldap).to receive(:search_root_dse).and_return(root_dse)
        expect(subject.get_naming_contexts(ldap)).to be(naming_contexts)
      end
    end
  end

  describe '#discover_base_dn' do
    let(:ldap) do
      instance_double(Net::LDAP)
    end

    context 'No naming contexts' do
      it do
        expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(nil)
        expect(subject.discover_base_dn(ldap)).to be(nil)
      end
    end

    context 'Invalid naming contexts' do
      let(:invalid_naming_contexts) do
        %w[invalid1 invalid2]
      end
      it do
        expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(invalid_naming_contexts)
        expect(subject.discover_base_dn(ldap)).to be(nil)
      end
    end

    context 'Valid naming contexts' do
      let(:base_dn) do
        'DC=abcdef'
      end
      let(:valid_naming_contexts) do
        [base_dn]
      end
      it do
        expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
        expect(subject.discover_base_dn(ldap)).to be(base_dn)
      end
    end

    context 'Valid naming contexts (lowercase dc)' do
      let(:base_dn) do
        'dc=abcdef'
      end
      let(:valid_naming_contexts) do
        [base_dn]
      end
      it do
        expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
        expect(subject.discover_base_dn(ldap)).to be(base_dn)
      end
    end
  end
end
